﻿namespace QND
{
    using Microsoft.EnterpriseManagement.HealthService;
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Xml;
    using Microsoft.Win32;
    using QND.SCOMMM.Tracing;
    //using Microsoft.EnterpriseManagement.Tracing;
    //using Microsoft.Internal.ManagedWPP;

 [MonitoringModule(ModuleType.Condition)]
 [ModuleOutput(true)]
    public sealed class DataItemEncoder : ModuleBase<EncodedDataItem>
    {
        /// <summary>
        /// Keep track if we must return a single data item or a collection of data items
        /// This field is marked as readonly since we set it in the constructor and should never change it.
        /// </summary>
        private readonly bool returnMultipleItems;
        /// <summary>
        /// Sets a text file where to dump the processed data items and thier results.
        /// This field is marked as readonly since we set it in the constructor and should never change it.
        /// The field is not documented since it's for advanced troubleshooting only and can impair module performance
        /// </summary>
        private readonly string traceFilePath;
        
        /// <summary>
        /// Object we use for doing locking to check to see if we have been
        /// shutdown or not.  This field is marked as readonly since we set it
        /// in the constructor and should never change it.
        /// </summary>
        private readonly object shutdownLock;

        #region XML Element Names

        /// <summary>
        /// The outer element of configuration XML to a module is always
        /// "Configuration".
        /// </summary>
        private const string ConfigurationElementName = "Configuration";

        /// <summary>
        /// Our own portion of configuration has the element name
        /// "ReturnMultipleItems".
        /// </summary>
        private const string ReturnMultipleItems = "ReturnMultipleItems";

        #endregion

        #region Tracing Messages constants
        const int TRCMSG_NewDataItem = 1;
        const int TRCMSG_PostDataItem = 2;
        const int TRCMSG_Exception = 3;
        const int TRCMSG_Config = 4;
        const int TRCMSG_ConfigExit = 5;
        const int TRCGUID_Config = 0;
         const int TRCGUID_DataItem = 1;
        #endregion

        /// <summary>
        /// Boolean value tracking if the module has been shutdown or not.
        /// </summary>
        private bool shutdown;

        /// <summary>
        /// Constructor of the Module is called when the Module is initialized with the moduleHost pointer and the Xml configuration
        /// along with the previous state, if any. If this is a stateful module, whatever state the module stored last would be handed 
        /// back to the module. This is just a byte array.
        /// </summary>
        /// <param name="moduleHost">The host object provides the services the
        /// module needs to interact with the Health Service.</param>
        /// <param name="configuration">XML reader giving the configuration of this module.</param>
        /// <param name="previousState">Previous state of the module.  This must be null since this module never calls SaveState</param>
        public DataItemEncoder(
            ModuleHost<EncodedDataItem> moduleHost,
            XmlReader configuration,
            byte[] previousState)
            : base(moduleHost)
        {
            TraceHelper.WriteEventString(TraceHelper.TraceLevel.Verbose, TraceHelper.TraceFlags.Verbose, 0, 1, "Enter DataItemEncoder Constructor");
            // Verify parameters given by the Health Service.
            if (moduleHost == null)
            {
                TraceHelper.WriteEventString(TraceHelper.TraceLevel.Fatal, TraceHelper.TraceFlags.Error, 0, 2, "Error moduleHost is null");
                throw new ArgumentNullException("moduleHost");
            }

            if (null == configuration)
            {
                TraceHelper.WriteEventString(TraceHelper.TraceLevel.Fatal, TraceHelper.TraceFlags.Error, 0, 2, "Error configuration is null");
                throw new ArgumentNullException("configuration");
            }

            if (previousState != null)
            {
                TraceHelper.WriteEventString(TraceHelper.TraceLevel.Fatal, TraceHelper.TraceFlags.Error, 0, 2, "Error previousState is null");
                // Since this module never calls SaveState this value should be null.
                throw new ArgumentOutOfRangeException("previousState");
            }

            // Create the shutdown block
            this.shutdownLock = new object();

            // set the default
            this.returnMultipleItems = true;
            this.traceFilePath = null;
            // Config blob that we expect is of the form:
            // <Configuration>
            //      <ReturnMultipleItems>true|false</ReturnMultipleItems>
            // </Configuration>

            try
            {
                configuration.MoveToContent();

                //configuration.ReadElementString(DataItemEncoder.TraceFilePath);
                //configuration.ReadElementAsBoolean(DataItemEncoder.ReturnMultipleItems);

                configuration.ReadStartElement(DataItemEncoder.ConfigurationElementName);
                configuration.ReadStartElement(DataItemEncoder.ReturnMultipleItems);
                this.returnMultipleItems = configuration.ReadContentAsBoolean();
                /*
                 * not used anymore we now read from registry
                configuration.ReadEndElement();
                configuration.ReadStartElement(DataItemEncoder.TraceFilePath);
                this.traceFilePath = configuration.ReadContentAsString();
                 */
                configuration.ReadEndElement();
                configuration.ReadEndElement(); 
                try
                {
                    this.traceFilePath = (string)Registry.GetValue(@"HKEY_LOCAL_MACHINE\Software\Microsoft\Microsoft Operations Manager\3.0\Modules\Global\QND.DataItemEncoder", "LogFile",null);
                    if (string.IsNullOrEmpty(traceFilePath)) { traceFilePath = null; }
                }
                catch
                {
                    this.traceFilePath = null;
                }
                if (!String.IsNullOrEmpty(traceFilePath)) { TraceHelper.WriteEventString(TraceHelper.TraceLevel.Information, TraceHelper.TraceFlags.Error, 0, 3, String.Format("Dump file specified:{0}", traceFilePath)); }

            }
            catch (XmlException xe)
            {
                /* It is important to catch all known exceptions.  From the
                 * module constructor if there is just a generic error then it
                 * should be wrapped as the inner exception to ModuleException
                 * and thrown.  It is also fine to have more detailed events 
                 * and use NotifyError with an event id.
                 */
                TraceHelper.WriteEventString(TraceHelper.TraceLevel.Fatal, TraceHelper.TraceFlags.Error, 0, 2, String.Format("Error configuration is invalid. {0}", configuration.ReadOuterXml()));
                throw new ModuleException(Resources.XmlConfigError, xe);
            }
            finally
            {
                TraceHelper.WriteEventString(TraceHelper.TraceLevel.Verbose, TraceHelper.TraceFlags.Verbose, 0, 1, "Exiting DataItemEncoder Constructor");
            }
        }

        /// <summary>
        /// Do any cleanup that you have to do in this method
        /// </summary>
        public override void Shutdown()
        {
            lock (this.shutdownLock)
            {
                Debug.Assert(!this.shutdown);

                this.shutdown = true;
            }
        }

        /// <summary>
        /// This function is called by the Health Service, so it is a necessary method. You should explicitly call RequestNextDataItem() to
        /// receive data in this method
        /// </summary>
        public override void Start()
        {
            // since Shutdown, OnNewDataItems and Start acquire the lock before doing any action, only one of them can execute code in the code
            // segment inside the lock { ... } code block
            lock (this.shutdownLock)
            {
                if (this.shutdown)
                {
                    return;
                }

                // Request the first data batch.
                this.ModuleHost.RequestNextDataItem();
            }
        }

        /// <summary>
        /// This is the function that is called when new data is available. We
        /// read the data items and append the configured string to it and output it
        /// immediately
        /// </summary>
        /// <param name="dataItems">data received</param>
        /// <param name="logicalSet">Is the data batch a logical set</param>
        /// <param name="acknowledgedCallback">Optional. Callback to be invoked when the module accepts responsibility for the data item.</param>
        /// <param name="acknowledgedState">Parameter that must be passed in the call to acknowledgedCallback</param>
        /// <param name="completionCallback">Optional. Callback to be invoked when the module has completed processing this data batch.</param>
        /// <param name="completionState">Parameter that must be passed in the call to completionCallback</param>
        //// It is critical have this attribute. For every input (number of inputs is defined in the MP), there should be a corresponding input method
        //// InputStream(0) says that this is the input method for Input-0. If we did not have this methodAttribute, Health Service would be unable to find
        //// an input method for port0. (ports are numbered from 0 to (MaxInput -1)). If there are 5 Inputs defined for the Module in the MP,
        //// then there should be a correponding method with InputStream(0), InputStream(1)...InputStream(4). Although it is possible to have a 
        //// single method handle all the inputs, it would not be able to tell which port the input came from since the port nunmber is not passed
        //// along with the data, although the method may able to tell which port the input came from based on the DataItem type
        [InputStream(0)]
        public void OnNewDataItems(
            DataItemBase[] dataItems,
            bool logicalSet,
            DataItemAcknowledgementCallback acknowledgedCallback,
            object acknowledgedState,
            DataItemProcessingCompleteCallback completionCallback,
            object completionState)
        {
            // Either both delegates are null or neither should be.
            if ((acknowledgedCallback == null && completionCallback != null) ||
                (acknowledgedCallback != null && completionCallback == null))
            {
                throw new ArgumentOutOfRangeException(
                    "acknowledgedCallback, completionCallback",
                    Resources.AckError);
            }

            // If an ack was requested on the data we received we want to
            // request an ack on the data we post.  If there was no ack on the
            // input data it doesn't make sense to request an ack on the
            // output.
            bool ackNeeded = acknowledgedCallback != null;

            // Acquire the lock guarding against shutdown.
            lock (this.shutdownLock)
            {
                // If the module has been shutdown we should stop processing.
                if (this.shutdown)
                {
                    return;
                }

                // Create the list we will use for storing output
                List<EncodedDataItem> outputDataItems = new List<EncodedDataItem>();
                string allIn1Item="";
                bool corruptLogicalSet = false;

                // Loop through all input data items processing them
                foreach (DataItemBase dataItem in dataItems)
                {
                    // When processing the input data items as a best practice
                    // we should always check for MalformedDataItemException on
                    // any access to the input data.  
                    System.IO.StreamWriter logStream = null;
                    try {
                        if (this.traceFilePath != null) { logStream = System.IO.File.AppendText(this.traceFilePath); }
                    }
                    catch {
                        // if we have any issue opening the logStream just ignore it
                        logStream = null;
                    }
                    Trace2File t2f = delegate(System.IO.StreamWriter log, string line) { if (log != null) { log.WriteLine(line); log.Flush(); } };
                    try {

                        //get the whole input DataItem
                        XmlReader reader = dataItem.GetItemXml();
                        reader.MoveToContent();
                        string plainTextItem = reader.ReadOuterXml();
                        t2f(logStream,plainTextItem);
                        TraceHelper.WriteEventString(TraceHelper.TraceLevel.Verbose, TraceHelper.TraceFlags.Debug, 1, 1, String.Format("Input Item: {0}",plainTextItem));
                        // write the encoded form of the DataItem into encodedString
                        // don't know a lot about the StringBuilder class, it's clear that the initial length will be just a start6ing point since the encoded item will be longer
                        System.Text.StringBuilder encodedString = new System.Text.StringBuilder(plainTextItem.Length);
                        XmlWriterSettings writerSettings = new  XmlWriterSettings();
                        writerSettings.OmitXmlDeclaration = true;
                        writerSettings.ConformanceLevel = ConformanceLevel.Fragment;
                        XmlWriter writer = XmlWriter.Create(encodedString,writerSettings);
                        writer.WriteString(plainTextItem);
                        writer.Close();
                        string encodedXml = encodedString.ToString();
                        t2f(logStream,"Encoded as follow");
                        t2f(logStream,encodedXml);
                        TraceHelper.WriteEventString(TraceHelper.TraceLevel.Verbose, TraceHelper.TraceFlags.Debug, 1, 2, String.Format("Encoded Item: {0}", encodedXml));

                        // if we return multiple itemas default just queue the new dataitem, if not build the output string allIn1Item
                        if (this.returnMultipleItems)
                        {
                            EncodedDataItem outputDataItem = new EncodedDataItem(encodedXml);
                            TraceHelper.WriteEventString(TraceHelper.TraceLevel.Verbose, TraceHelper.TraceFlags.Debug, 1, 2, String.Format("Output Item: {0}", reader.ReadOuterXml()));
                            t2f(logStream, "Output data item");
                            reader = outputDataItem.GetItemXml();
                            reader.MoveToContent();
                            t2f(logStream, reader.ReadOuterXml());
                            t2f(logStream, "---------");

                            outputDataItems.Add(outputDataItem);
                        }
                        else
                        {
                            TraceHelper.WriteEventString(TraceHelper.TraceLevel.Verbose, TraceHelper.TraceFlags.Debug, 1, 3, "Queuing data item in a batch");
                            allIn1Item += encodedXml;
                        }
                    }
                    catch (MalformedDataItemException) {
                        // The usual case for this is a coding bug.  On a debug
                        // build we want to go to the debugger to investigate.
                        Debug.Assert(false);
                        TraceHelper.WriteEventString(TraceHelper.TraceLevel.Error, TraceHelper.TraceFlags.Error, 1, 4, "Malformed DataItem Exception");
                        // On malformed data we need to drop the data item.  If
                        // the batch is a logical set we drop the entire batch
                        // since we shouldn't break up the set.
                        if (logicalSet)
                        {
                            corruptLogicalSet = true;
                            this.ModuleHost.NotifyDroppedMalformedDataItems(dataItems.Length);
                            TraceHelper.WriteEventString(TraceHelper.TraceLevel.Error, TraceHelper.TraceFlags.Error, 1, 4, "Malformed DataItem Exception - Dropping all data item aborting the workflow");
                            // We are going to drop the whole batch so we can
                            // stop processing.
                            break;
                        }
                        else
                        {
                            // Not a set we can drop out this individual item only.
                            TraceHelper.WriteEventString(TraceHelper.TraceLevel.Error, TraceHelper.TraceFlags.Error, 1, 4, "Malformed DataItem Exception - Dropping this data item and continuing");
                            // Notify the host that we dropped a data item.
                            this.ModuleHost.NotifyDroppedMalformedDataItems(1);
                        }
                    }
                    finally
                    {
                        if (logStream != null) { 
                            logStream.Close();
                            logStream.Dispose();
                        }

                    }
                }

                if (!this.returnMultipleItems && !String.IsNullOrEmpty(allIn1Item)) //if we need to return a single item
                {
                    try {
                        TraceHelper.WriteEventString(TraceHelper.TraceLevel.Verbose, TraceHelper.TraceFlags.Verbose, 1, 2, "Outputting a DataItem with the sum of all the input data items");
                        EncodedDataItem outputDataItem = new EncodedDataItem(allIn1Item);
                        outputDataItems.Add(outputDataItem);
                    }
                    catch (MalformedDataItemException) {
                        // The usual case for this is a coding bug.  On a debug
                        // build we want to go to the debugger to investigate.
                        Debug.Assert(false);
                        TraceHelper.WriteEventString(TraceHelper.TraceLevel.Error, TraceHelper.TraceFlags.Error, 1, 4, "Malformed DataItem Exception");
                        corruptLogicalSet = true;
                        this.ModuleHost.NotifyDroppedMalformedDataItems(dataItems.Length);
                    }
                }
                
                // If we ended up with no data items due to corruption or any
                // items in a logical set were corrupted we will have no
                // output.  We need to give any acks and then request the next
                // data item.
                if (outputDataItems.Count == 0 || corruptLogicalSet)
                {
                    if (ackNeeded)
                    {
                        acknowledgedCallback(acknowledgedState);
                        completionCallback(completionState);
                    }

                    this.ModuleHost.RequestNextDataItem();

                    return;
                }

                if (ackNeeded)
                {
                    // We want to forward on the acknowledgement on input data
                    // to the next module.  We create an anonymous delegate to
                    // process handling the ack.
                    DataItemAcknowledgementCallback ackDelegate = delegate(object ackState)
                    {
                        // We set this parameter to null when calling
                        // PostOutputDataItems so we expect this parameter to be null here.
                        Debug.Assert(ackState == null);

                        lock (this.shutdownLock)
                        {
                            // If we have been shutdown stop processing.
                            if (this.shutdown)
                            {
                                return;
                            }

                            // Send the ack and completion back for the input.
                            acknowledgedCallback(acknowledgedState);
                            completionCallback(completionState);

                            // Know that we have sent back both the completion and
                            // ack we can request the next data item.
                            this.ModuleHost.RequestNextDataItem();
                        }
                    };

                    this.ModuleHost.PostOutputDataItems(outputDataItems.ToArray(), logicalSet, ackDelegate, null);
                }
                else
                {
                    // No ack was requested on input.  We can post the output
                    // and then immediately request the next data items
                    this.ModuleHost.PostOutputDataItems(outputDataItems.ToArray(), logicalSet);

                    this.ModuleHost.RequestNextDataItem();
                }
            }
        }

        delegate void Trace2File(System.IO.StreamWriter log, string line);
    }
}
